import { vec2 } from 'gl-matrix';
import { ChangeTypes, Events } from '../../enums';
import {
  getEnabledElement,
  utilities as csUtils,
  getEnabledElementByViewportId,
} from '@cornerstonejs/core';
import type { Types } from '@cornerstonejs/core';

import { AnnotationTool } from '../base';
import {
  addAnnotation,
  getAnnotations,
  removeAnnotation,
} from '../../stateManagement/annotation/annotationState';

import { drawTextBox as drawTextBoxSvg } from '../../drawingSvg';
import { state } from '../../store/state';
import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds';
import {
  triggerAnnotationCompleted,
  triggerAnnotationModified,
} from '../../stateManagement/annotation/helpers/state';

import {
  resetElementCursor,
  hideElementCursor,
} from '../../cursors/elementCursor';

import type {
  EventTypes,
  PublicToolProps,
  ToolProps,
  SVGDrawingHelper,
  Annotation,
  InteractionTypes,
  ToolHandle,
} from '../../types';
import type { LabelAnnotation } from '../../types/ToolSpecificAnnotationTypes';
import type { StyleSpecifier } from '../../types/AnnotationStyle';
import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';

class LabelTool extends AnnotationTool {
  static toolName = 'Label';

  editData: {
    annotation: Annotation;
    viewportIdsToRender: string[];
    newAnnotation?: boolean;
    hasMoved?: boolean;
    offset: Types.Point3;
  } | null;
  isDrawing: boolean;
  isHandleOutsideImage: boolean;

  constructor(
    toolProps: PublicToolProps = {},
    defaultToolProps: ToolProps = {
      supportedInteractionTypes: ['Mouse', 'Touch'],
      configuration: {
        shadow: true,
        getTextCallback,
        changeTextCallback,
        preventHandleOutsideImage: false,
      },
    }
  ) {
    super(toolProps, defaultToolProps);
  }

  isPointNearTool = (
    element: HTMLDivElement,
    annotation: LabelAnnotation,
    canvasCoords: Types.Point2,
    proximity: number
  ): boolean => {
    const enabledElement = getEnabledElement(element);
    const { viewport } = enabledElement;

    const { annotationUID } = annotation;
    const point = annotation.data.handles.points[0];
    const annotationCanvasCoordinate = viewport.worldToCanvas(point);

    // Check if point is near the label point
    const dist = vec2.distance(canvasCoords, annotationCanvasCoordinate);
    if (dist < proximity) {
      return true;
    }

    // Check if point is over the text box
    const svgLayer = element.querySelector('svg');
    if (!svgLayer) {
      return false;
    }

    // Find text group with matching annotation UID
    const textGroup = svgLayer.querySelector(
      `g[data-annotation-uid="${annotationUID}"]`
    );
    if (!textGroup) {
      return false;
    }

    // Get bounding box of text group
    const textGroupElement = textGroup as SVGGraphicsElement;
    const bbox = textGroupElement.getBBox();
    const transform = textGroupElement.getAttribute('transform');

    // Parse the transform string to get translation values
    let translateX = 0;
    let translateY = 0;

    if (transform) {
      const matches = transform.match(/translate\(([-\d.]+)\s+([-\d.]+)\)/);
      if (matches) {
        translateX = parseFloat(matches[1]);
        translateY = parseFloat(matches[2]);
      }
    }

    // Adjust bbox with transform
    const x = bbox.x + translateX;
    const y = bbox.y + translateY;

    // Check if point is inside text box bounds
    const isNear =
      canvasCoords[0] >= x &&
      canvasCoords[0] <= x + bbox.width &&
      canvasCoords[1] >= y &&
      canvasCoords[1] <= y + bbox.height;

    return isNear;
  };

  static hydrate = (
    viewportId: string,
    position: Types.Point3,
    label: string,
    options?: {
      annotationUID?: string;
    }
  ): LabelAnnotation => {
    const enabledElement = getEnabledElementByViewportId(viewportId);
    if (!enabledElement) {
      return;
    }
    const { viewport } = enabledElement;
    const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();

    const { viewPlaneNormal, viewUp } = viewport.getCamera();

    // This is a workaround to access the protected method getReferencedImageId
    // we should make those static too
    const instance = new this();

    const referencedImageId = instance.getReferencedImageId(
      viewport,
      position,
      viewPlaneNormal,
      viewUp
    );

    const annotation = {
      annotationUID: options?.annotationUID || csUtils.uuidv4(),
      data: {
        label,
        handles: {
          points: [position],
        },
      },
      highlighted: false,
      autoGenerated: false,
      invalidated: false,
      isLocked: false,
      isVisible: true,
      metadata: {
        toolName: instance.getToolName(),
        viewPlaneNormal,
        FrameOfReferenceUID,
        referencedImageId,
        ...options,
      },
    };

    addAnnotation(annotation, viewport.element);

    triggerAnnotationRenderForViewportIds([viewport.id]);
  };

  /**
   * Based on the current position of the mouse and the current imageId to create
   * a Length Annotation and stores it in the annotationManager
   *
   * @param evt -  EventTypes.NormalizedMouseEventType
   * @returns The annotation object.
   *
   */
  addNewAnnotation = (
    evt: EventTypes.InteractionEventType
  ): LabelAnnotation => {
    const eventDetail = evt.detail;
    const { currentPoints, element } = eventDetail;
    const worldPos = currentPoints.world;

    hideElementCursor(element);
    this.isDrawing = true;

    const annotation = <LabelAnnotation>(
      this.createAnnotation(evt, [
        <Types.Point3>[...worldPos],
        <Types.Point3>[...worldPos],
      ])
    );

    addAnnotation(annotation, element);

    const viewportIdsToRender = getViewportIdsWithToolToRender(
      element,
      this.getToolName()
    );

    this.editData = {
      annotation,
      newAnnotation: true,
      viewportIdsToRender,
      offset: [0, 0, 0],
    };

    evt.preventDefault();

    triggerAnnotationRenderForViewportIds(viewportIdsToRender);
    this.configuration.getTextCallback((label) => {
      if (!label) {
        removeAnnotation(annotation.annotationUID);
        triggerAnnotationRenderForViewportIds(viewportIdsToRender);
        this.isDrawing = false;
        return;
      }
      resetElementCursor(element);
      annotation.data.label = label;

      triggerAnnotationCompleted(annotation);

      triggerAnnotationRenderForViewportIds(viewportIdsToRender);
    });

    this.createMemo(element, annotation, { newAnnotation: true });

    return annotation;
  };

  handleSelectedCallback(
    _evt: EventTypes.InteractionEventType,
    _annotation: Annotation,
    _handle: ToolHandle,
    _interactionType: InteractionTypes
  ): void {}

  toolSelectedCallback = (
    evt: EventTypes.InteractionEventType,
    annotation: LabelAnnotation
  ): void => {
    const eventDetail = evt.detail;
    const { element, currentPoints } = eventDetail;

    annotation.highlighted = true;

    const viewportIdsToRender = getViewportIdsWithToolToRender(
      element,
      this.getToolName()
    );

    // Capture the offset between the annotation's anchor and the pointer position
    let offset: Types.Point3 = [0, 0, 0];
    if (currentPoints && currentPoints.world) {
      const initialWorldPos = currentPoints.world;
      const anchorWorldPos = annotation.data.handles.points[0];
      offset = [
        anchorWorldPos[0] - initialWorldPos[0],
        anchorWorldPos[1] - initialWorldPos[1],
        anchorWorldPos[2] - initialWorldPos[2],
      ];
    }

    this.editData = {
      annotation,
      viewportIdsToRender,
      offset,
    };

    this._activateModify(element);

    hideElementCursor(element);

    triggerAnnotationRenderForViewportIds(viewportIdsToRender);

    evt.preventDefault();
  };

  _endCallback = (evt: EventTypes.InteractionEventType): void => {
    const eventDetail = evt.detail;
    const { element } = eventDetail;

    const { annotation, viewportIdsToRender, newAnnotation } = this.editData;
    this._deactivateDraw(element);
    this._deactivateModify(element);

    resetElementCursor(element);

    if (newAnnotation) {
      this.createMemo(element, annotation, { newAnnotation });
    }

    this.editData = null;
    this.isDrawing = false;
    this.doneEditMemo();

    if (
      this.isHandleOutsideImage &&
      this.configuration.preventHandleOutsideImage
    ) {
      removeAnnotation(annotation.annotationUID);
    }

    triggerAnnotationRenderForViewportIds(viewportIdsToRender);

    if (newAnnotation) {
      triggerAnnotationCompleted(annotation);
    }
  };

  _dragCallback = (evt: EventTypes.InteractionEventType): void => {
    const eventDetail = evt.detail;
    const { currentPoints, element } = eventDetail;
    const worldPos = currentPoints.world;

    const { annotation, viewportIdsToRender, offset } = this.editData;

    if (offset) {
      // without offset the move/drag will have a sudden weird jump
      annotation.data.handles.points[0] = [
        worldPos[0] + offset[0],
        worldPos[1] + offset[1],
        worldPos[2] + offset[2],
      ];
    } else {
      annotation.data.handles.points[0] = [...worldPos];
    }
    annotation.invalidated = true;

    triggerAnnotationRenderForViewportIds(viewportIdsToRender);

    // Dispatching annotation modified
    triggerAnnotationModified(annotation, element, ChangeTypes.LabelChange);
  };

  _doneChangingTextCallback(element, annotation, updatedLabel): void {
    annotation.data.label = updatedLabel;

    const viewportIdsToRender = getViewportIdsWithToolToRender(
      element,
      this.getToolName()
    );
    triggerAnnotationRenderForViewportIds(viewportIdsToRender);

    // Dispatching annotation modified
    triggerAnnotationModified(annotation, element);
  }

  cancel = (element: HTMLDivElement) => {
    if (this.isDrawing) {
      this.isDrawing = false;
      this._deactivateModify(element);
      resetElementCursor(element);

      const { annotation, viewportIdsToRender, newAnnotation } = this.editData;
      const { data } = annotation;

      annotation.highlighted = false;
      data.handles.activeHandleIndex = null;

      triggerAnnotationRenderForViewportIds(viewportIdsToRender);

      if (newAnnotation) {
        triggerAnnotationCompleted(annotation);
      }

      this.editData = null;
      return annotation.annotationUID;
    }
  };

  _activateModify = (element: HTMLDivElement) => {
    state.isInteractingWithTool = true;

    element.addEventListener(
      Events.MOUSE_UP,
      this._endCallback as EventListener
    );
    element.addEventListener(
      Events.MOUSE_DRAG,
      this._dragCallback as EventListener
    );
    element.addEventListener(
      Events.MOUSE_CLICK,
      this._endCallback as EventListener
    );

    element.addEventListener(
      Events.TOUCH_TAP,
      this._endCallback as EventListener
    );
    element.addEventListener(
      Events.TOUCH_END,
      this._endCallback as EventListener
    );
    element.addEventListener(
      Events.TOUCH_DRAG,
      this._dragCallback as EventListener
    );
  };

  _deactivateModify = (element: HTMLDivElement) => {
    state.isInteractingWithTool = false;

    element.removeEventListener(
      Events.MOUSE_UP,
      this._endCallback as EventListener
    );
    element.removeEventListener(
      Events.MOUSE_DRAG,
      this._dragCallback as EventListener
    );
    element.removeEventListener(
      Events.MOUSE_CLICK,
      this._endCallback as EventListener
    );

    element.removeEventListener(
      Events.TOUCH_TAP,
      this._endCallback as EventListener
    );
    element.removeEventListener(
      Events.TOUCH_DRAG,
      this._dragCallback as EventListener
    );
    element.removeEventListener(
      Events.TOUCH_END,
      this._endCallback as EventListener
    );
  };

  _activateDraw = (element: HTMLDivElement) => {
    state.isInteractingWithTool = true;

    element.addEventListener(
      Events.MOUSE_UP,
      this._endCallback as EventListener
    );
    element.addEventListener(
      Events.MOUSE_DRAG,
      this._dragCallback as EventListener
    );
    element.addEventListener(
      Events.MOUSE_MOVE,
      this._dragCallback as EventListener
    );
    element.addEventListener(
      Events.MOUSE_CLICK,
      this._endCallback as EventListener
    );

    element.addEventListener(
      Events.TOUCH_TAP,
      this._endCallback as EventListener
    );
    element.addEventListener(
      Events.TOUCH_END,
      this._endCallback as EventListener
    );
    element.addEventListener(
      Events.TOUCH_DRAG,
      this._dragCallback as EventListener
    );
  };

  _deactivateDraw = (element: HTMLDivElement) => {
    state.isInteractingWithTool = false;

    element.removeEventListener(
      Events.MOUSE_UP,
      this._endCallback as EventListener
    );
    element.removeEventListener(
      Events.MOUSE_DRAG,
      this._dragCallback as EventListener
    );
    element.removeEventListener(
      Events.MOUSE_MOVE,
      this._dragCallback as EventListener
    );
    element.removeEventListener(
      Events.MOUSE_CLICK,
      this._endCallback as EventListener
    );

    element.removeEventListener(
      Events.TOUCH_TAP,
      this._endCallback as EventListener
    );
    element.removeEventListener(
      Events.TOUCH_END,
      this._endCallback as EventListener
    );
    element.removeEventListener(
      Events.TOUCH_DRAG,
      this._dragCallback as EventListener
    );
  };

  /**
   * it is used to draw the length annotation in each
   * request animation frame. It calculates the updated cached statistics if
   * data is invalidated and cache it.
   *
   * @param enabledElement - The Cornerstone's enabledElement.
   * @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing.
   */
  renderAnnotation = (
    enabledElement: Types.IEnabledElement,
    svgDrawingHelper: SVGDrawingHelper
  ): boolean => {
    let renderStatus = false;
    const { viewport } = enabledElement;
    const { element } = viewport;

    let annotations = getAnnotations(this.getToolName(), element);

    // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
    if (!annotations?.length) {
      return renderStatus;
    }

    annotations = this.filterInteractableAnnotationsForElement(
      element,
      annotations
    );

    const styleSpecifier: StyleSpecifier = {
      toolGroupId: this.toolGroupId,
      toolName: this.getToolName(),
      viewportId: enabledElement.viewport.id,
    };

    // Draw SVG
    for (let i = 0; i < annotations.length; i++) {
      const annotation = annotations[i] as LabelAnnotation;
      const { annotationUID, data } = annotation;
      const point = data.handles.points[0];

      styleSpecifier.annotationUID = annotationUID;

      const canvasCoordinates = viewport.worldToCanvas(point);

      renderStatus = true;

      // If rendering engine has been destroyed while rendering
      if (!viewport.getRenderingEngine()) {
        console.warn('Rendering Engine has been destroyed');
        return renderStatus;
      }

      if (!isAnnotationVisible(annotationUID)) {
        continue;
      }

      if (!data.label) {
        continue;
      }

      const options = this.getLinkedTextBoxStyle(styleSpecifier, annotation);

      const textBoxUID = '1';
      drawTextBoxSvg(
        svgDrawingHelper,
        annotationUID,
        textBoxUID,
        [data.label],
        canvasCoordinates,
        {
          ...options,
          padding: 0,
        }
      );
    }

    return renderStatus;
  };

  _isInsideVolume(index1, index2, dimensions) {
    return (
      csUtils.indexWithinDimensions(index1, dimensions) &&
      csUtils.indexWithinDimensions(index2, dimensions)
    );
  }
}

function getTextCallback(doneChangingTextCallback) {
  return doneChangingTextCallback(prompt('Enter your annotation:'));
}

function changeTextCallback(data, eventData, doneChangingTextCallback) {
  return doneChangingTextCallback(prompt('Enter your annotation:'));
}

LabelTool.toolName = 'Label';
export default LabelTool;
